# What is Stack in Data Structure? - javatpoint
# https://www.javatpoint.com/data-structure-stack
"""
**Stack: Concept, Properties, and Operations**

1. Concept:
   - A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle.
   - It represents a collection of elements where the addition and removal of elements can only be done at one end called the top of the stack.
   - The last element added to the stack is the first one to be removed.

2. Properties:
   - LIFO (Last-In-First-Out): The element that is most recently added to the stack is the first one to be removed.
   - Size: The stack has a size that represents the number of elements currently present in the stack.
   - Empty Stack: When the stack is empty, it means it does not contain any elements.
   - Full Stack: Depending on the implementation, a stack may have a maximum capacity. If the stack reaches its maximum capacity, it is considered full.

3. Operations:
   - Push: Adds an element to the top of the stack.
   - Pop: Removes and returns the element from the top of the stack.
   - Peek (Top): Returns the element at the top of the stack without removing it.
   - Is Empty: Checks whether the stack is empty or not.
   - Is Full: Checks whether the stack is full or not (if it has a maximum capacity).
   - Size: Returns the number of elements in the stack.

   Example:
   Let's consider a stack with elements [4, 7, 2, 9]:
   - Push(5): [4, 7, 2, 9, 5]
   - Pop(): [4, 7, 2, 9]
   - Peek(): 9
   - Is Empty: False
   - Size: 4

The stack data structure is widely used in various applications such as function call stacks, expression evaluation, undo/redo functionality, and depth-first search algorithms. It provides an efficient way to manage data in a Last-In-First-Out manner.
"""

class Stack:
    """
    A Stack follows LIFO (Last In First Out) principle.

    Attributes:
        stack_list: List to hold stack data.
    """

    def __init__(self):
        """
        Initialize an empty stack.
        """
        self.stack_list = []

    def push(self, data):
        """
        Pushes an item onto the top of this stack.

        Args:
            data: Data to be pushed onto stack.
        """
        self.stack_list.append(data)

    def pop(self):
        """
        Removes the object at the top of this stack and returns that object as the value of this function.

        Returns:
            The data from the top of stack. If stack is empty, returns None.
        """
        if self.stack_list:
            return self.stack_list.pop()
        return None

    def peek(self):
        """
        Looks at the object at the top of this stack without removing it from the stack.

        Returns:
            The data from the top of stack. If stack is empty, returns None.
        """
        if self.stack_list:
            return self.stack_list[-1]
        return None

    def is_empty(self):
        """
        Tests if this stack is empty.

        Returns:
            True if and only if this stack contains no items; False otherwise.
        """
        return len(self.stack_list) == 0

    def __len__(self):
        """
        Returns the number of elements in this stack.

        Returns:
            The number of elements in this stack.
        """
        return len(self.stack_list)

    def __repr__(self):
        """
        Returns a string that represents a valid Python expression that could be used to recreate this stack.

        Returns:
            A string that represents a valid Python expression.
        """
        return f'Stack(size={len(self.stack_list)}, top={self.peek()})'

    def __str__(self):
        """
        Returns a string representing the stack. The string representation consists of the stack's elements and the words 'Top:' and 'Bottom', enclosed in square brackets ("[]"). Adjacent elements are separated by the characters "->" (a space, a hyphen, and a space).

        Returns:
            A string representing the stack.
        """
        result = ['Top:']
        result.extend(reversed([str(x) for x in self.stack_list]))
        result.append('Bottom')
        return ' -> '.join(result)


if __name__ == "__main__":
    stack = Stack()

    # push data onto the stack
    for i in range(5):
        stack.push(i)

    # print the stack
    print(f"Stack: {stack}")  # Stack: Top: -> 4 -> 3 -> 2 -> 1 -> 0 -> Bottom

    # print the stack representation
    print(repr(stack))  # Stack(size=5, top=4)

    # peek at the top of the stack
    print(f"Peek: {stack.peek()}")  # Peek: 4

    # pop data from the stack
    print(f"Pop: {stack.pop()}")  # Pop: 4

    # print the stack after popping
    print(f"Stack after pop: {stack}")  # Stack after pop: Top: -> 3 -> 2 -> 1 -> 0 -> Bottom

    # check if the stack is empty
    print(f"Is stack empty: {stack.is_empty()}")  # Is stack empty: False

    # get the number of elements in the stack using len()
    print(f"Stack size: {len(stack)}")  # Stack size: 4
